/**
 * Isotope v1.5.25
 * An exquisite jQuery plugin for magical layouts
 * http://isotope.metafizzy.co
 *
 * Commercial use requires one-time purchase of a commercial license
 * http://isotope.metafizzy.co/docs/license.html
 *
 * Non-commercial use is licensed under the MIT License
 *
 * Copyright 2013 Metafizzy
 */

/*jshint asi: true, browser: true, curly: true, eqeqeq: true, forin: false, immed: false, newcap: true, noempty: true, strict: true, undef: true */
/*global jQuery: false */

(function( window, $, undefined ){

    'use strict';

    // get global vars
    var document = window.document;
    var Modernizr = window.Modernizr;

    // helper function
    var capitalize = function( str ) {
        return str.charAt(0).toUpperCase() + str.slice(1);
    };

    // ========================= getStyleProperty by kangax ===============================
    // http://perfectionkills.com/feature-testing-css-properties/

    var prefixes = 'Moz Webkit O Ms'.split(' ');

    var getStyleProperty = function( propName ) {
        var style = document.documentElement.style,
            prefixed;

        // test standard property first
        if ( typeof style[propName] === 'string' ) {
            return propName;
        }

        // capitalize
        propName = capitalize( propName );

        // test vendor specific properties
        for ( var i=0, len = prefixes.length; i < len; i++ ) {
            prefixed = prefixes[i] + propName;
            if ( typeof style[ prefixed ] === 'string' ) {
                return prefixed;
            }
        }
    };

    var transformProp = getStyleProperty('transform'),
        transitionProp = getStyleProperty('transitionProperty');


    // ========================= miniModernizr ===============================
    // <3<3<3 and thanks to Faruk and Paul for doing the heavy lifting

    /*!
     * Modernizr v1.6ish: miniModernizr for Isotope
     * http://www.modernizr.com
     *
     * Developed by:
     * - Faruk Ates  http://farukat.es/
     * - Paul Irish  http://paulirish.com/
     *
     * Copyright (c) 2009-2010
     * Dual-licensed under the BSD or MIT licenses.
     * http://www.modernizr.com/license/
     */

    /*
     * This version whittles down the script just to check support for
     * CSS transitions, transforms, and 3D transforms.
    */

    var tests = {
        csstransforms: function() {
            return !!transformProp;
        },

        csstransforms3d: function() {
            var test = !!getStyleProperty('perspective');
            // double check for Chrome's false positive
            if ( test ) {
                var vendorCSSPrefixes = ' -o- -moz- -ms- -webkit- -khtml- '.split(' '),
                    mediaQuery = '@media (' + vendorCSSPrefixes.join('transform-3d),(') + 'modernizr)',
                    $style = $('<style>' + mediaQuery + '{#modernizr{height:3px}}' + '</style>')
                        .appendTo('head'),
                    $div = $('<div id="modernizr" />').appendTo('html');

                test = $div.height() === 3;

                $div.remove();
                $style.remove();
            }
            return test;
        },

        csstransitions: function() {
            return !!transitionProp;
        }
    };

    var testName;

    if ( Modernizr ) {
        // if there's a previous Modernzir, check if there are necessary tests
        for ( testName in tests) {
            if ( !Modernizr.hasOwnProperty( testName ) ) {
                // if test hasn't been run, use addTest to run it
                Modernizr.addTest( testName, tests[ testName ] );
            }
        }
    } else {
        // or create new mini Modernizr that just has the 3 tests
        Modernizr = window.Modernizr = {
            _version : '1.6ish: miniModernizr for Isotope'
        };

        var classes = ' ';
        var result;

        // Run through tests
        for ( testName in tests) {
            result = tests[ testName ]();
            Modernizr[ testName ] = result;
            classes += ' ' + ( result ?  '' : 'no-' ) + testName;
        }

        // Add the new classes to the <html> element.
        $('html').addClass( classes );
    }


    // ========================= isoTransform ===============================

    /**
     *  provides hooks for .css({ scale: value, translate: [x, y] })
     *  Progressively enhanced CSS transforms
     *  Uses hardware accelerated 3D transforms for Safari
     *  or falls back to 2D transforms.
     */

    if ( Modernizr.csstransforms ) {

        // i.e. transformFnNotations.scale(0.5) >> 'scale3d( 0.5, 0.5, 1)'
        var transformFnNotations = Modernizr.csstransforms3d ?
            { // 3D transform functions
                translate : function ( position ) {
                    return 'translate3d(' + position[0] + 'px, ' + position[1] + 'px, 0) ';
                },
                scale : function ( scale ) {
                    return 'scale3d(' + scale + ', ' + scale + ', 1) ';
                }
            } :
            { // 2D transform functions
                translate : function ( position ) {
                    return 'translate(' + position[0] + 'px, ' + position[1] + 'px) ';
                },
                scale : function ( scale ) {
                    return 'scale(' + scale + ') ';
                }
            }
        ;

        var setIsoTransform = function ( elem, name, value ) {
            // unpack current transform data
            var data =  $.data( elem, 'isoTransform' ) || {},
                newData = {},
                fnName,
                transformObj = {},
                transformValue;

            // i.e. newData.scale = 0.5
            newData[ name ] = value;
            // extend new value over current data
            $.extend( data, newData );

            for ( fnName in data ) {
                transformValue = data[ fnName ];
                transformObj[ fnName ] = transformFnNotations[ fnName ]( transformValue );
            }

            // get proper order
            // ideally, we could loop through this give an array, but since we only have
            // a couple transforms we're keeping track of, we'll do it like so
            var translateFn = transformObj.translate || '',
                scaleFn = transformObj.scale || '',
                // sorting so translate always comes first
                valueFns = translateFn + scaleFn;

            // set data back in elem
            $.data( elem, 'isoTransform', data );

            // set name to vendor specific property
            elem.style[ transformProp ] = valueFns;
        };

        // ==================== scale ===================

        $.cssNumber.scale = true;

        $.cssHooks.scale = {
            set: function( elem, value ) {
                // uncomment this bit if you want to properly parse strings
                // if ( typeof value === 'string' ) {
                //   value = parseFloat( value );
                // }
                setIsoTransform( elem, 'scale', value );
            },
            get: function( elem, computed ) {
                var transform = $.data( elem, 'isoTransform' );
                return transform && transform.scale ? transform.scale : 1;
            }
        };

        $.fx.step.scale = function( fx ) {
            $.cssHooks.scale.set( fx.elem, fx.now+fx.unit );
        };


        // ==================== translate ===================

        $.cssNumber.translate = true;

        $.cssHooks.translate = {
            set: function( elem, value ) {

                // uncomment this bit if you want to properly parse strings
                // if ( typeof value === 'string' ) {
                //   value = value.split(' ');
                // }
                //
                // var i, val;
                // for ( i = 0; i < 2; i++ ) {
                //   val = value[i];
                //   if ( typeof val === 'string' ) {
                //     val = parseInt( val );
                //   }
                // }

                setIsoTransform( elem, 'translate', value );
            },

            get: function( elem, computed ) {
                var transform = $.data( elem, 'isoTransform' );
                return transform && transform.translate ? transform.translate : [ 0, 0 ];
            }
        };

    }

    // ========================= get transition-end event ===============================
    var transitionEndEvent, transitionDurProp;

    if ( Modernizr.csstransitions ) {
        transitionEndEvent = {
            WebkitTransitionProperty: 'webkitTransitionEnd',  // webkit
            MozTransitionProperty: 'transitionend',
            OTransitionProperty: 'oTransitionEnd otransitionend',
            transitionProperty: 'transitionend'
        }[ transitionProp ];

        transitionDurProp = getStyleProperty('transitionDuration');
    }

    // ========================= smartresize ===============================

    /*
     * smartresize: debounced resize event for jQuery
     *
     * latest version and complete README available on Github:
     * https://github.com/louisremi/jquery.smartresize.js
     *
     * Copyright 2011 @louis_remi
     * Licensed under the MIT license.
     */

    var $event = $.event,
        dispatchMethod = $.event.handle ? 'handle' : 'dispatch',
        resizeTimeout;

    $event.special.smartresize = {
        setup: function() {
            $(this).bind( "resize", $event.special.smartresize.handler );
        },
        teardown: function() {
            $(this).unbind( "resize", $event.special.smartresize.handler );
        },
        handler: function( event, execAsap ) {
            // Save the context
            var context = this,
                args = arguments;

            // set correct event type
            event.type = "smartresize";

            if ( resizeTimeout ) { clearTimeout( resizeTimeout ); }
            resizeTimeout = setTimeout(function() {
                $event[ dispatchMethod ].apply( context, args );
            }, execAsap === "execAsap"? 0 : 100 );
        }
    };

    $.fn.smartresize = function( fn ) {
        return fn ? this.bind( "smartresize", fn ) : this.trigger( "smartresize", ["execAsap"] );
    };



// ========================= Isotope ===============================


    // our "Widget" object constructor
    $.Isotope = function( options, element, callback ){
        this.element = $( element );

        this._create( options );
        this._init( callback );
    };

    // styles of container element we want to keep track of
    var isoContainerStyles = [ 'width', 'height' ];

    var $window = $(window);

    $.Isotope.settings = {
        resizable: true,
        layoutMode : 'masonry',
        containerClass : 'isotope',
        itemClass : 'isotope-item',
        hiddenClass : 'isotope-hidden',
        hiddenStyle: { opacity: 0, scale: 0.001 },
        visibleStyle: { opacity: 1, scale: 1 },
        containerStyle: {
            position: 'relative',
            overflow: 'hidden'
        },
        animationEngine: 'best-available',
        animationOptions: {
            queue: false,
            duration: 800
        },
        sortBy : 'original-order',
        sortAscending : true,
        resizesContainer : true,
        transformsEnabled: true,
        itemPositionDataEnabled: false
    };

    $.Isotope.prototype = {

        // sets up widget
        _create : function( options ) {

            this.options = $.extend( {}, $.Isotope.settings, options );

            this.styleQueue = [];
            this.elemCount = 0;

            // get original styles in case we re-apply them in .destroy()
            var elemStyle = this.element[0].style;
            this.originalStyle = {};
            // keep track of container styles
            var containerStyles = isoContainerStyles.slice(0);
            for ( var prop in this.options.containerStyle ) {
                containerStyles.push( prop );
            }
            for ( var i=0, len = containerStyles.length; i < len; i++ ) {
                prop = containerStyles[i];
                this.originalStyle[ prop ] = elemStyle[ prop ] || '';
            }
            // apply container style from options
            this.element.css( this.options.containerStyle );

            this._updateAnimationEngine();
            this._updateUsingTransforms();

            // sorting
            var originalOrderSorter = {
                'original-order' : function( $elem, instance ) {
                    instance.elemCount ++;
                    return instance.elemCount;
                },
                random : function() {
                    return Math.random();
                }
            };

            this.options.getSortData = $.extend( this.options.getSortData, originalOrderSorter );

            // need to get atoms
            this.reloadItems();

            // get top left position of where the bricks should be
            this.offset = {
                left: parseInt( ( this.element.css('padding-left') || 0 ), 10 ),
                top: parseInt( ( this.element.css('padding-top') || 0 ), 10 )
            };

            // add isotope class first time around
            var instance = this;
            setTimeout( function() {
                instance.element.addClass( instance.options.containerClass );
            }, 0 );

            // bind resize method
            if ( this.options.resizable ) {
                $window.bind( 'smartresize.isotope', function() {
                    instance.resize();
                });
            }

            // dismiss all click events from hidden events
            this.element.delegate( '.' + this.options.hiddenClass, 'click', function(){
                return false;
            });

        },

        _getAtoms : function( $elems ) {
            var selector = this.options.itemSelector,
                // filter & find
                $atoms = selector ? $elems.filter( selector ).add( $elems.find( selector ) ) : $elems,
                // base style for atoms
                atomStyle = { position: 'absolute' };

            // filter out text nodes
            $atoms = $atoms.filter( function( i, atom ) {
                return atom.nodeType === 1;
            });

            if ( this.usingTransforms ) {
                atomStyle.left = 0;
                atomStyle.top = 0;
            }

            $atoms.css( atomStyle ).addClass( this.options.itemClass );

            this.updateSortData( $atoms, true );

            return $atoms;
        },

        // _init fires when your instance is first created
        // (from the constructor above), and when you
        // attempt to initialize the widget again (by the bridge)
        // after it has already been initialized.
        _init : function( callback ) {

            this.$filteredAtoms = this._filter( this.$allAtoms );
            this._sort();
            this.reLayout( callback );

        },

        option : function( opts ){
            // change options AFTER initialization:
            // signature: $('#foo').bar({ cool:false });
            if ( $.isPlainObject( opts ) ){
                this.options = $.extend( true, this.options, opts );

                // trigger _updateOptionName if it exists
                var updateOptionFn;
                for ( var optionName in opts ) {
                    updateOptionFn = '_update' + capitalize( optionName );
                    if ( this[ updateOptionFn ] ) {
                        this[ updateOptionFn ]();
                    }
                }
            }
        },

        // ====================== updaters ====================== //
        // kind of like setters

        _updateAnimationEngine : function() {
            var animationEngine = this.options.animationEngine.toLowerCase().replace( /[ _\-]/g, '');
            var isUsingJQueryAnimation;
            // set applyStyleFnName
            switch ( animationEngine ) {
                case 'css' :
                case 'none' :
                    isUsingJQueryAnimation = false;
                    break;
                case 'jquery' :
                    isUsingJQueryAnimation = true;
                    break;
                default : // best available
                    isUsingJQueryAnimation = !Modernizr.csstransitions;
            }
            this.isUsingJQueryAnimation = isUsingJQueryAnimation;
            this._updateUsingTransforms();
        },

        _updateTransformsEnabled : function() {
            this._updateUsingTransforms();
        },

        _updateUsingTransforms : function() {
            var usingTransforms = this.usingTransforms = this.options.transformsEnabled &&
                Modernizr.csstransforms && Modernizr.csstransitions && !this.isUsingJQueryAnimation;

            // prevent scales when transforms are disabled
            if ( !usingTransforms ) {
                delete this.options.hiddenStyle.scale;
                delete this.options.visibleStyle.scale;
            }

            this.getPositionStyles = usingTransforms ? this._translate : this._positionAbs;
        },


        // ====================== Filtering ======================

        _filter : function( $atoms ) {
            var filter = this.options.filter === '' ? '*' : this.options.filter;

            if ( !filter ) {
                return $atoms;
            }

            var hiddenClass    = this.options.hiddenClass,
                hiddenSelector = '.' + hiddenClass,
                $hiddenAtoms   = $atoms.filter( hiddenSelector ),
                $atomsToShow   = $hiddenAtoms;

            if ( filter !== '*' ) {
                $atomsToShow = $hiddenAtoms.filter( filter );
                var $atomsToHide = $atoms.not( hiddenSelector ).not( filter ).addClass( hiddenClass );
                this.styleQueue.push({ $el: $atomsToHide, style: this.options.hiddenStyle });
            }

            this.styleQueue.push({ $el: $atomsToShow, style: this.options.visibleStyle });
            $atomsToShow.removeClass( hiddenClass );

            return $atoms.filter( filter );
        },

        // ====================== Sorting ======================

        updateSortData : function( $atoms, isIncrementingElemCount ) {
            var instance = this,
                getSortData = this.options.getSortData,
                $this, sortData;
            $atoms.each(function(){
                $this = $(this);
                sortData = {};
                // get value for sort data based on fn( $elem ) passed in
                for ( var key in getSortData ) {
                    if ( !isIncrementingElemCount && key === 'original-order' ) {
                        // keep original order original
                        sortData[ key ] = $.data( this, 'isotope-sort-data' )[ key ];
                    } else {
                        sortData[ key ] = getSortData[ key ]( $this, instance );
                    }
                }
                // apply sort data to element
                $.data( this, 'isotope-sort-data', sortData );
            });
        },

        // used on all the filtered atoms
        _sort : function() {

            var sortBy = this.options.sortBy,
                getSorter = this._getSorter,
                sortDir = this.options.sortAscending ? 1 : -1,
                sortFn = function( alpha, beta ) {
                    var a = getSorter( alpha, sortBy ),
                        b = getSorter( beta, sortBy );
                    // fall back to original order if data matches
                    if ( a === b && sortBy !== 'original-order') {
                        a = getSorter( alpha, 'original-order' );
                        b = getSorter( beta, 'original-order' );
                    }
                    return ( ( a > b ) ? 1 : ( a < b ) ? -1 : 0 ) * sortDir;
                };

            this.$filteredAtoms.sort( sortFn );
        },

        _getSorter : function( elem, sortBy ) {
            return $.data( elem, 'isotope-sort-data' )[ sortBy ];
        },

        // ====================== Layout Helpers ======================

        _translate : function( x, y ) {
            return { translate : [ x, y ] };
        },

        _positionAbs : function( x, y ) {
            return { left: x, top: y };
        },

        _pushPosition : function( $elem, x, y ) {
            x = Math.round( x + this.offset.left );
            y = Math.round( y + this.offset.top );
            var position = this.getPositionStyles( x, y );
            this.styleQueue.push({ $el: $elem, style: position });
            if ( this.options.itemPositionDataEnabled ) {
                $elem.data('isotope-item-position', {x: x, y: y} );
            }
        },


        // ====================== General Layout ======================

        // used on collection of atoms (should be filtered, and sorted before )
        // accepts atoms-to-be-laid-out to start with
        layout : function( $elems, callback ) {

            var layoutMode = this.options.layoutMode;

            // layout logic
            this[ '_' +  layoutMode + 'Layout' ]( $elems );

            // set the size of the container
            if ( this.options.resizesContainer ) {
                var containerStyle = this[ '_' +  layoutMode + 'GetContainerSize' ]();
                this.styleQueue.push({ $el: this.element, style: containerStyle });
            }

            this._processStyleQueue( $elems, callback );

            this.isLaidOut = true;
        },

        _processStyleQueue : function( $elems, callback ) {
            // are we animating the layout arrangement?
            // use plugin-ish syntax for css or animate
            var styleFn = !this.isLaidOut ? 'css' : (
                    this.isUsingJQueryAnimation ? 'animate' : 'css'
                ),
                animOpts = this.options.animationOptions,
                onLayout = this.options.onLayout,
                objStyleFn, processor,
                triggerCallbackNow, callbackFn;

            // default styleQueue processor, may be overwritten down below
            processor = function( i, obj ) {
                obj.$el[ styleFn ]( obj.style, animOpts );
            };

            if ( this._isInserting && this.isUsingJQueryAnimation ) {
                // if using styleQueue to insert items
                processor = function( i, obj ) {
                    // only animate if it not being inserted
                    objStyleFn = obj.$el.hasClass('no-transition') ? 'css' : styleFn;
                    obj.$el[ objStyleFn ]( obj.style, animOpts );
                };

            } else if ( callback || onLayout || animOpts.complete ) {
                // has callback
                var isCallbackTriggered = false,
                    // array of possible callbacks to trigger
                    callbacks = [ callback, onLayout, animOpts.complete ],
                    instance = this;
                triggerCallbackNow = true;
                // trigger callback only once
                callbackFn = function() {
                    if ( isCallbackTriggered ) {
                        return;
                    }
                    var hollaback;
                    for (var i=0, len = callbacks.length; i < len; i++) {
                        hollaback = callbacks[i];
                        if ( typeof hollaback === 'function' ) {
                            hollaback.call( instance.element, $elems, instance );
                        }
                    }
                    isCallbackTriggered = true;
                };

                if ( this.isUsingJQueryAnimation && styleFn === 'animate' ) {
                    // add callback to animation options
                    animOpts.complete = callbackFn;
                    triggerCallbackNow = false;

                } else if ( Modernizr.csstransitions ) {
                    // detect if first item has transition
                    var i = 0,
                        firstItem = this.styleQueue[0],
                        testElem = firstItem && firstItem.$el,
                        styleObj;
                    // get first non-empty jQ object
                    while ( !testElem || !testElem.length ) {
                        styleObj = this.styleQueue[ i++ ];
                        // HACK: sometimes styleQueue[i] is undefined
                        if ( !styleObj ) {
                            return;
                        }
                        testElem = styleObj.$el;
                    }
                    // get transition duration of the first element in that object
                    // yeah, this is inexact
                    var duration = parseFloat( getComputedStyle( testElem[0] )[ transitionDurProp ] );
                    if ( duration > 0 ) {
                        processor = function( i, obj ) {
                            obj.$el[ styleFn ]( obj.style, animOpts )
                            // trigger callback at transition end
                                .one( transitionEndEvent, callbackFn );
                        };
                        triggerCallbackNow = false;
                    }
                }
            }

            // process styleQueue
            $.each( this.styleQueue, processor );

            if ( triggerCallbackNow ) {
                callbackFn();
            }

            // clear out queue for next time
            this.styleQueue = [];
        },


        resize : function() {
            if ( this[ '_' + this.options.layoutMode + 'ResizeChanged' ]() ) {
                this.reLayout();
            }
        },


        reLayout : function( callback ) {

            this[ '_' +  this.options.layoutMode + 'Reset' ]();
            this.layout( this.$filteredAtoms, callback );

        },

        // ====================== Convenience methods ======================

        // ====================== Adding items ======================

        // adds a jQuery object of items to a isotope container
        addItems : function( $content, callback ) {
            var $newAtoms = this._getAtoms( $content );
            // add new atoms to atoms pools
            this.$allAtoms = this.$allAtoms.add( $newAtoms );

            if ( callback ) {
                callback( $newAtoms );
            }
        },

        // convienence method for adding elements properly to any layout
        // positions items, hides them, then animates them back in <--- very sezzy
        insert : function( $content, callback ) {
            // position items
            this.element.append( $content );

            var instance = this;
            this.addItems( $content, function( $newAtoms ) {
                var $newFilteredAtoms = instance._filter( $newAtoms );
                instance._addHideAppended( $newFilteredAtoms );
                instance._sort();
                instance.reLayout();
                instance._revealAppended( $newFilteredAtoms, callback );
            });

        },

        // convienence method for working with Infinite Scroll
        appended : function( $content, callback ) {
            var instance = this;
            this.addItems( $content, function( $newAtoms ) {
                instance._addHideAppended( $newAtoms );
                instance.layout( $newAtoms );
                instance._revealAppended( $newAtoms, callback );
            });
        },

        // adds new atoms, then hides them before positioning
        _addHideAppended : function( $newAtoms ) {
            this.$filteredAtoms = this.$filteredAtoms.add( $newAtoms );
            $newAtoms.addClass('no-transition');

            this._isInserting = true;

            // apply hidden styles
            this.styleQueue.push({ $el: $newAtoms, style: this.options.hiddenStyle });
        },

        // sets visible style on new atoms
        _revealAppended : function( $newAtoms, callback ) {
            var instance = this;
            // apply visible style after a sec
            setTimeout( function() {
                // enable animation
                $newAtoms.removeClass('no-transition');
                // reveal newly inserted filtered elements
                instance.styleQueue.push({ $el: $newAtoms, style: instance.options.visibleStyle });
                instance._isInserting = false;
                instance._processStyleQueue( $newAtoms, callback );
            }, 10 );
        },

        // gathers all atoms
        reloadItems : function() {
            this.$allAtoms = this._getAtoms( this.element.children() );
        },

        // removes elements from Isotope widget
        remove: function( $content, callback ) {
            // remove elements immediately from Isotope instance
            this.$allAtoms = this.$allAtoms.not( $content );
            this.$filteredAtoms = this.$filteredAtoms.not( $content );
            // remove() as a callback, for after transition / animation
            var instance = this;
            var removeContent = function() {
                $content.remove();
                if ( callback ) {
                    callback.call( instance.element );
                }
            };

            if ( $content.filter( ':not(.' + this.options.hiddenClass + ')' ).length ) {
                // if any non-hidden content needs to be removed
                this.styleQueue.push({ $el: $content, style: this.options.hiddenStyle });
                this._sort();
                this.reLayout( removeContent );
            } else {
                // remove it now
                removeContent();
            }

        },

        shuffle : function( callback ) {
            this.updateSortData( this.$allAtoms );
            this.options.sortBy = 'random';
            this._sort();
            this.reLayout( callback );
        },

        // destroys widget, returns elements and container back (close) to original style
        destroy : function() {

            var usingTransforms = this.usingTransforms;
            var options = this.options;

            this.$allAtoms
                .removeClass( options.hiddenClass + ' ' + options.itemClass )
                .each(function(){
                    var style = this.style;
                    style.position = '';
                    style.top = '';
                    style.left = '';
                    style.opacity = '';
                    if ( usingTransforms ) {
                        style[ transformProp ] = '';
                    }
                });

            // re-apply saved container styles
            var elemStyle = this.element[0].style;
            for ( var prop in this.originalStyle ) {
                elemStyle[ prop ] = this.originalStyle[ prop ];
            }

            this.element
                .unbind('.isotope')
                .undelegate( '.' + options.hiddenClass, 'click' )
                .removeClass( options.containerClass )
                .removeData('isotope');

            $window.unbind('.isotope');

        },


        // ====================== LAYOUTS ======================

        // calculates number of rows or columns
        // requires columnWidth or rowHeight to be set on namespaced object
        // i.e. this.masonry.columnWidth = 200
        _getSegments : function( isRows ) {
            var namespace = this.options.layoutMode,
                measure  = isRows ? 'rowHeight' : 'columnWidth',
                size     = isRows ? 'height' : 'width',
                segmentsName = isRows ? 'rows' : 'cols',
                containerSize = this.element[ size ](),
                segments,
                // i.e. options.masonry && options.masonry.columnWidth
                segmentSize = this.options[ namespace ] && this.options[ namespace ][ measure ] ||
                    // or use the size of the first item, i.e. outerWidth
                    this.$filteredAtoms[ 'outer' + capitalize(size) ](true) ||
                    // if there's no items, use size of container
                    containerSize;

            segments = Math.floor( containerSize / segmentSize );
            segments = Math.max( segments, 1 );

            // i.e. this.masonry.cols = ....
            this[ namespace ][ segmentsName ] = segments;
            // i.e. this.masonry.columnWidth = ...
            this[ namespace ][ measure ] = segmentSize;

        },

        _checkIfSegmentsChanged : function( isRows ) {
            var namespace = this.options.layoutMode,
                segmentsName = isRows ? 'rows' : 'cols',
                prevSegments = this[ namespace ][ segmentsName ];
            // update cols/rows
            this._getSegments( isRows );
            // return if updated cols/rows is not equal to previous
            return ( this[ namespace ][ segmentsName ] !== prevSegments );
        },

        // ====================== Masonry ======================

        _masonryReset : function() {
            // layout-specific props
            this.masonry = {};
            // FIXME shouldn't have to call this again
            this._getSegments();
            var i = this.masonry.cols;
            this.masonry.colYs = [];
            while (i--) {
                this.masonry.colYs.push( 0 );
            }
        },

        _masonryLayout : function( $elems ) {
            var instance = this,
                props = instance.masonry;
            $elems.each(function(){
                var $this  = $(this),
                    //how many columns does this brick span
                    colSpan = Math.ceil( $this.outerWidth(true) / props.columnWidth );
                colSpan = Math.min( colSpan, props.cols );

                if ( colSpan === 1 ) {
                    // if brick spans only one column, just like singleMode
                    instance._masonryPlaceBrick( $this, props.colYs );
                } else {
                    // brick spans more than one column
                    // how many different places could this brick fit horizontally
                    var groupCount = props.cols + 1 - colSpan,
                        groupY = [],
                        groupColY,
                        i;

                    // for each group potential horizontal position
                    for ( i=0; i < groupCount; i++ ) {
                        // make an array of colY values for that one group
                        groupColY = props.colYs.slice( i, i+colSpan );
                        // and get the max value of the array
                        groupY[i] = Math.max.apply( Math, groupColY );
                    }

                    instance._masonryPlaceBrick( $this, groupY );
                }
            });
        },

        // worker method that places brick in the columnSet
        //   with the the minY
        _masonryPlaceBrick : function( $brick, setY ) {
            // get the minimum Y value from the columns
            var minimumY = Math.min.apply( Math, setY ),
                shortCol = 0;

            // Find index of short column, the first from the left
            for (var i=0, len = setY.length; i < len; i++) {
                if ( setY[i] === minimumY ) {
                    shortCol = i;
                    break;
                }
            }

            // position the brick
            var x = this.masonry.columnWidth * shortCol,
                y = minimumY;
            this._pushPosition( $brick, x, y );

            // apply setHeight to necessary columns
            var setHeight = minimumY + $brick.outerHeight(true),
                setSpan = this.masonry.cols + 1 - len;
            for ( i=0; i < setSpan; i++ ) {
                this.masonry.colYs[ shortCol + i ] = setHeight;
            }

        },

        _masonryGetContainerSize : function() {
            var containerHeight = Math.max.apply( Math, this.masonry.colYs );
            return { height: containerHeight };
        },

        _masonryResizeChanged : function() {
            return this._checkIfSegmentsChanged();
        },

        // ====================== fitRows ======================

        _fitRowsReset : function() {
            this.fitRows = {
                x : 0,
                y : 0,
                height : 0
            };
        },

        _fitRowsLayout : function( $elems ) {
            var instance = this,
                containerWidth = this.element.width(),
                props = this.fitRows;

            $elems.each( function() {
                var $this = $(this),
                    atomW = $this.outerWidth(true),
                    atomH = $this.outerHeight(true);

                if ( props.x !== 0 && atomW + props.x > containerWidth ) {
                    // if this element cannot fit in the current row
                    props.x = 0;
                    props.y = props.height;
                }

                // position the atom
                instance._pushPosition( $this, props.x, props.y );

                props.height = Math.max( props.y + atomH, props.height );
                props.x += atomW;

            });
        },

        _fitRowsGetContainerSize : function () {
            return { height : this.fitRows.height };
        },

        _fitRowsResizeChanged : function() {
            return true;
        },


        // ====================== cellsByRow ======================

        _cellsByRowReset : function() {
            this.cellsByRow = {
                index : 0
            };
            // get this.cellsByRow.columnWidth
            this._getSegments();
            // get this.cellsByRow.rowHeight
            this._getSegments(true);
        },

        _cellsByRowLayout : function( $elems ) {
            var instance = this,
                props = this.cellsByRow;
            $elems.each( function(){
                var $this = $(this),
                    col = props.index % props.cols,
                    row = Math.floor( props.index / props.cols ),
                    x = ( col + 0.5 ) * props.columnWidth - $this.outerWidth(true) / 2,
                    y = ( row + 0.5 ) * props.rowHeight - $this.outerHeight(true) / 2;
                instance._pushPosition( $this, x, y );
                props.index ++;
            });
        },

        _cellsByRowGetContainerSize : function() {
            return { height : Math.ceil( this.$filteredAtoms.length / this.cellsByRow.cols ) * this.cellsByRow.rowHeight + this.offset.top };
        },

        _cellsByRowResizeChanged : function() {
            return this._checkIfSegmentsChanged();
        },


        // ====================== straightDown ======================

        _straightDownReset : function() {
            this.straightDown = {
                y : 0
            };
        },

        _straightDownLayout : function( $elems ) {
            var instance = this;
            $elems.each( function( i ){
                var $this = $(this);
                instance._pushPosition( $this, 0, instance.straightDown.y );
                instance.straightDown.y += $this.outerHeight(true);
            });
        },

        _straightDownGetContainerSize : function() {
            return { height : this.straightDown.y };
        },

        _straightDownResizeChanged : function() {
            return true;
        },


        // ====================== masonryHorizontal ======================

        _masonryHorizontalReset : function() {
            // layout-specific props
            this.masonryHorizontal = {};
            // FIXME shouldn't have to call this again
            this._getSegments( true );
            var i = this.masonryHorizontal.rows;
            this.masonryHorizontal.rowXs = [];
            while (i--) {
                this.masonryHorizontal.rowXs.push( 0 );
            }
        },

        _masonryHorizontalLayout : function( $elems ) {
            var instance = this,
                props = instance.masonryHorizontal;
            $elems.each(function(){
                var $this  = $(this),
                    //how many rows does this brick span
                    rowSpan = Math.ceil( $this.outerHeight(true) / props.rowHeight );
                rowSpan = Math.min( rowSpan, props.rows );

                if ( rowSpan === 1 ) {
                    // if brick spans only one column, just like singleMode
                    instance._masonryHorizontalPlaceBrick( $this, props.rowXs );
                } else {
                    // brick spans more than one row
                    // how many different places could this brick fit horizontally
                    var groupCount = props.rows + 1 - rowSpan,
                        groupX = [],
                        groupRowX, i;

                    // for each group potential horizontal position
                    for ( i=0; i < groupCount; i++ ) {
                        // make an array of colY values for that one group
                        groupRowX = props.rowXs.slice( i, i+rowSpan );
                        // and get the max value of the array
                        groupX[i] = Math.max.apply( Math, groupRowX );
                    }

                    instance._masonryHorizontalPlaceBrick( $this, groupX );
                }
            });
        },

        _masonryHorizontalPlaceBrick : function( $brick, setX ) {
            // get the minimum Y value from the columns
            var minimumX  = Math.min.apply( Math, setX ),
                smallRow  = 0;
            // Find index of smallest row, the first from the top
            for (var i=0, len = setX.length; i < len; i++) {
                if ( setX[i] === minimumX ) {
                    smallRow = i;
                    break;
                }
            }

            // position the brick
            var x = minimumX,
                y = this.masonryHorizontal.rowHeight * smallRow;
            this._pushPosition( $brick, x, y );

            // apply setHeight to necessary columns
            var setWidth = minimumX + $brick.outerWidth(true),
                setSpan = this.masonryHorizontal.rows + 1 - len;
            for ( i=0; i < setSpan; i++ ) {
                this.masonryHorizontal.rowXs[ smallRow + i ] = setWidth;
            }
        },

        _masonryHorizontalGetContainerSize : function() {
            var containerWidth = Math.max.apply( Math, this.masonryHorizontal.rowXs );
            return { width: containerWidth };
        },

        _masonryHorizontalResizeChanged : function() {
            return this._checkIfSegmentsChanged(true);
        },


        // ====================== fitColumns ======================

        _fitColumnsReset : function() {
            this.fitColumns = {
                x : 0,
                y : 0,
                width : 0
            };
        },

        _fitColumnsLayout : function( $elems ) {
            var instance = this,
                containerHeight = this.element.height(),
                props = this.fitColumns;
            $elems.each( function() {
                var $this = $(this),
                    atomW = $this.outerWidth(true),
                    atomH = $this.outerHeight(true);

                if ( props.y !== 0 && atomH + props.y > containerHeight ) {
                    // if this element cannot fit in the current column
                    props.x = props.width;
                    props.y = 0;
                }

                // position the atom
                instance._pushPosition( $this, props.x, props.y );

                props.width = Math.max( props.x + atomW, props.width );
                props.y += atomH;

            });
        },

        _fitColumnsGetContainerSize : function () {
            return { width : this.fitColumns.width };
        },

        _fitColumnsResizeChanged : function() {
            return true;
        },



        // ====================== cellsByColumn ======================

        _cellsByColumnReset : function() {
            this.cellsByColumn = {
                index : 0
            };
            // get this.cellsByColumn.columnWidth
            this._getSegments();
            // get this.cellsByColumn.rowHeight
            this._getSegments(true);
        },

        _cellsByColumnLayout : function( $elems ) {
            var instance = this,
                props = this.cellsByColumn;
            $elems.each( function(){
                var $this = $(this),
                    col = Math.floor( props.index / props.rows ),
                    row = props.index % props.rows,
                    x = ( col + 0.5 ) * props.columnWidth - $this.outerWidth(true) / 2,
                    y = ( row + 0.5 ) * props.rowHeight - $this.outerHeight(true) / 2;
                instance._pushPosition( $this, x, y );
                props.index ++;
            });
        },

        _cellsByColumnGetContainerSize : function() {
            return { width : Math.ceil( this.$filteredAtoms.length / this.cellsByColumn.rows ) * this.cellsByColumn.columnWidth };
        },

        _cellsByColumnResizeChanged : function() {
            return this._checkIfSegmentsChanged(true);
        },

        // ====================== straightAcross ======================

        _straightAcrossReset : function() {
            this.straightAcross = {
                x : 0
            };
        },

        _straightAcrossLayout : function( $elems ) {
            var instance = this;
            $elems.each( function( i ){
                var $this = $(this);
                instance._pushPosition( $this, instance.straightAcross.x, 0 );
                instance.straightAcross.x += $this.outerWidth(true);
            });
        },

        _straightAcrossGetContainerSize : function() {
            return { width : this.straightAcross.x };
        },

        _straightAcrossResizeChanged : function() {
            return true;
        }

    };


    // ======================= imagesLoaded Plugin ===============================
    /*!
     * jQuery imagesLoaded plugin v1.1.0
     * http://github.com/desandro/imagesloaded
     *
     * MIT License. by Paul Irish et al.
     */


    // $('#my-container').imagesLoaded(myFunction)
    // or
    // $('img').imagesLoaded(myFunction)

    // execute a callback when all images have loaded.
    // needed because .load() doesn't work on cached images

    // callback function gets image collection as argument
    //  `this` is the container

    $.fn.imagesLoaded = function( callback ) {
        var $this = this,
            $images = $this.find('img').add( $this.filter('img') ),
            len = $images.length,
            blank = '',
            loaded = [];

        function triggerCallback() {
            callback.call( $this, $images );
        }

        function imgLoaded( event ) {
            var img = event.target;
            if ( img.src !== blank && $.inArray( img, loaded ) === -1 ){
                loaded.push( img );
                if ( --len <= 0 ){
                    setTimeout( triggerCallback );
                    $images.unbind( '.imagesLoaded', imgLoaded );
                }
            }
        }

        // if no images, trigger immediately
        if ( !len ) {
            triggerCallback();
        }

        $images.bind( 'load.imagesLoaded error.imagesLoaded',  imgLoaded ).each( function() {
            // cached images don't fire load sometimes, so we reset src.
            var src = this.src;
            // webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f
            // data uri bypasses webkit log warning (thx doug jones)
            this.src = blank;
            this.src = src;
        });

        return $this;
    };


    // helper function for logging errors
    // $.error breaks jQuery chaining
    var logError = function( message ) {
        if ( window.console ) {
            window.console.error( message );
        }
    };

    // =======================  Plugin bridge  ===============================
    // leverages data method to either create or return $.Isotope constructor
    // A bit from jQuery UI
    //   https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js
    // A bit from jcarousel
    //   https://github.com/jsor/jcarousel/blob/master/lib/jquery.jcarousel.js

    $.fn.isotope = function( options, callback ) {
        if ( typeof options === 'string' ) {
            // call method
            var args = Array.prototype.slice.call( arguments, 1 );

            this.each(function(){
                var instance = $.data( this, 'isotope' );
                if ( !instance ) {
                    logError( "cannot call methods on isotope prior to initialization; " +
                        "attempted to call method '" + options + "'" );
                    return;
                }
                if ( !$.isFunction( instance[options] ) || options.charAt(0) === "_" ) {
                    logError( "no such method '" + options + "' for isotope instance" );
                    return;
                }
                // apply method
                instance[ options ].apply( instance, args );
            });
        } else {
            this.each(function() {
                var instance = $.data( this, 'isotope' );
                if ( instance ) {
                    // apply options & init
                    instance.option( options );
                    instance._init( callback );
                } else {
                    // initialize new instance
                    $.data( this, 'isotope', new $.Isotope( options, this, callback ) );
                }
            });
        }
        // return jQuery object
        // so plugin methods do not have to
        return this;
    };

})( window, jQuery );